/*
 * Copyright 2012 Quytelda K. Gaiwin
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.tamalin.babel.engine;

import java.io.File;
import java.io.FileInputStream;
import java.util.Scanner;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created: Aug 13, 2011
 * @author Quytelda Gaiwin
 */
public class SequenceGenerator
{
	/**
	* Creates a new SequenceGenerator object that obtains definitions from the file referred to by definitions.
	* @param definitions the file to read sequence definitions from
	*/
	public SequenceGenerator(File definitions)
	{
		init((definitions != null) ? definitions : DEFAULT_DEFS);
		random = new SecureRandom();
	}

	/**
	* This method is called to initialize a SequenceGenerator object.
	* It read and parses the contents of the provided definitions file.
	* @param definitions the file from which to read sequence definitions
	*/
	private void init(File definitions)
	{
		// check to ensure the definitions file really exists
		if(!definitions.exists())
		{
			// if the file does not exists, complain and exit
			Logger.getLogger(SequenceGenerator.class.getName()).log(Level.SEVERE,"Provided definitions file doesn't exist!");
			System.exit(1);
		}

		// log the file reading event
		Logger.getLogger(SequenceGenerator.class.getName()).log(Level.INFO, "Reading definition file {0}", definitions.getPath());

		// attempt to read and parse each line of the definitions file
		try
		{
			// define a new scanner to read a FileInputStream from the definitions file
			Scanner in = new Scanner(new FileInputStream(definitions));

			while(in.hasNextLine())
			{
				// read the line
				String line = in.nextLine();

				// check that the line is not a comment
				// comments begin with # or ;
				if(!(line.startsWith("#") || line.startsWith(";")))
				{
					// seperate the key and the group members at the first instance of the equals sign
					String key = line.substring(0, line.indexOf('='));
					char[] members = line.substring(line.indexOf('=') + 1, line.length() - 1).toCharArray();

					// add the parsed key and members to a new CharSet object in the charsets arraylists
					charsets.add(new CharSet(key, members));
				}
			}
		}
		catch(IOException ex)
		{
			Logger.getLogger(SequenceGenerator.class.getName()).log(Level.WARNING,"Unable to load definitions.", ex);
		}
	}

	/**
	* Returns a securely generated random integer less than <code>max</code>.
	* @max the maximum desired return value
	*/
	private int randomInt(int max)
	{
		int r = Math.abs(random.nextInt() % max);
		return r;
	}

	/**
	* Generates a random sequence from the provided template.
	* @param template the template to use to generate the random sequence
	*/
	public char[] generate(String template)
	{
		// Split the template into it's components
		// The number of components in the template is the number of characters in the sequence.
		String[] keys = template.split(":");
		char[] seq = new char[keys.length];
		for(int i = 0; i < keys.length; i++)
		{
			// get the corresponding charset for the given template element
			CharSet set = findCharSet(keys[i]);

			// If the charset wasn't found, complain and continue
			if(set == null)
			{
				Logger.getLogger(SequenceGenerator.class.getName()).log(Level.WARNING, "Key {0} doesn't exist.");
				continue;
			}

			// pick random character and set it as the ith element in the sequence.
			int index = randomInt(set.getSize());
			seq[i] = set.getMember(index);
		}

		return seq;
	}

	/*
	* Identifies the charset with the given key.  If no charset with the given key exists, the method returns null.
	* @param key the key to search for
	*/
	private CharSet findCharSet(String key)
	{
		// Check each CharSet in charsets for a matching key
		// When am match is found, return the CharSet object
		for(CharSet charset : charsets)
			if(charset.getKey().equals(key)) return charset;

		// If no match is found, log the event and return null.
		Logger.getLogger(SequenceGenerator.class.getName()).log(Level.WARNING, "No charset with key {0}", key);
		return null;
	}

	private SecureRandom random;
	private static final File DEFAULT_DEFS = new File("/etc/babel/default");
	private ArrayList<CharSet> charsets = new ArrayList<CharSet>();
}

/**
* The CharSet class represents a series of characters and their unique identifying key.
*/
class CharSet
{
	/**
	* Constructs a new charset object from the given key and array of members
	* @param k they name of the CharSet, and the key that will be used to refer to it
	* @param m an array consisting of the members of the character set
	*/
	public CharSet(String k, char[] m)
	{
		key = k;
		members = m;
	}

	public char getMember(int i)
	{
		if(i >= members.length || i < 0) return members[0];
		return members[i];
	}

	public int getSize()
	{
		return members.length;
	}

	public String getKey()
	{
		return key;
	}

	private String key = null;
	private char[] members = null;
}
